package com.gitee.wsl.time


import com.gitee.wsl.ext.base.currentTimeZone
import com.gitee.wsl.ext.base.today
import com.gitee.wsl.ext.math.toIntExact
import com.gitee.wsl.math.Math
import kotlinx.datetime.*
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.atTime
import kotlinx.datetime.minus
import kotlinx.datetime.number
import kotlinx.datetime.plus
import kotlin.time.Instant
import kotlin.time.Duration
import kotlin.time.Duration.Companion.days
import kotlin.time.ExperimentalTime

internal fun  LocalDate.getProlepticMonth(): Long {
    return (year * 12L + monthNumber - 1)
}


/**
 * The earliest supported [Date], which can be used as a "far past" sentinel.
 */
@OptIn(ExperimentalTime::class)
val LocalDate.Companion.MIN  get() = LocalDate(Year.MIN_VALUE, Month.JANUARY, 1)

/**
 * The latest supported [Date], which can be used as a "far future" sentinel.
 */
@OptIn(ExperimentalTime::class)
val LocalDate.Companion.MAX get()  = LocalDate(Year.MAX_VALUE, Month.DECEMBER, 31)

fun LocalDate.toEpochMs(): Long = (this.toEpochDays()) * 24L * 60 * 60 * 1000

fun LocalDate.Companion.date() = LocalDate.now()
fun LocalDate.Companion.today() = date()
fun LocalDate.Companion.tomorrow() = today() + 1.days
fun LocalDate.Companion.nextWeek() = today() + 1.weeks
fun LocalDate.Companion.nextMonth() = today() + 1.months
fun LocalDate.Companion.nextYear() = today() + 1.years
fun LocalDate.Companion.yesterday() = today() - 1.days
fun LocalDate.Companion.lastWeek() = today() - 1.weeks
fun LocalDate.Companion.lastMonth() = today() - 1.months
fun LocalDate.Companion.lastYear() = today() - 1.years

fun LocalDate.next() = (this + 1.days).date

fun LocalDate.previous() = (this - 1.days).date

@ExperimentalTime
val LocalDate.isToday: Boolean
    get() = this == today

val LocalDate.isYesterday: Boolean
    get() = this == LocalDate.yesterday

val LocalDate.isTomorrow: Boolean
    get() = this == LocalDate.tomorrow

val LocalDate.isFuture: Boolean
    get() = this > today

val LocalDate.isPast: Boolean
    get() = this < today

val LocalDate.timestamp get() =  atStartOfDay().timestamp

fun Long.toLocalDate(): LocalDate = toLocalDateTime().date

inline val LocalDate.Companion.tomorrow: LocalDate
    get() = today + DatePeriod(0,0,1)

inline val LocalDate.Companion.yesterday: LocalDate
    get() = today - DatePeriod(0,0,1)


// region Week
inline val LocalDate.atStartOfWeek : LocalDate
    get() = this.minus(dayOfWeek.ordinal, DateTimeUnit.DAY)

inline val LocalDate.atEndOfWeek : LocalDate
    get() = atStartOfWeek.plus(6, DateTimeUnit.DAY)

val LocalDate.next: LocalDate get() =
    if (dayOfMonth != monthNumber.monthLength(isLeapYear(year))) {
        LocalDate(year, monthNumber, dayOfMonth + 1)
    } else if (monthNumber != 12) {
        LocalDate(year, monthNumber + 1, 1)
    } else {
        LocalDate(year + 1, 1, 1)
    }

val LocalDate.previous: LocalDate get() =
    if (dayOfMonth != 1) {
        LocalDate(year, monthNumber, dayOfMonth - 1)
    } else if (monthNumber != 1) {
        val newMonthNumber = monthNumber - 1
        LocalDate(year, newMonthNumber, newMonthNumber.monthLength(isLeapYear(year)))
    } else {
        LocalDate(year - 1, 12, 31)
    }

/**
 * Convenience function for creating a [LocalDate] using a [Year].
 *
 * @see [LocalDate]
 */
@ExperimentalTime
fun LocalDate(year: Year, month: Month, dayOfMonth: Int): LocalDate =
    LocalDate(year = year.value, month = month, dayOfMonth = dayOfMonth)

/**
 * Convenience function for creating a [LocalDate] using a [Year].
 *
 * @see [LocalDate]
 */
@ExperimentalTime
fun LocalDate(year: Year, monthNumber: Int=1, dayOfMonth: Int=1): LocalDate =
    LocalDate(year = year.value, monthNumber = monthNumber, dayOfMonth = dayOfMonth)



/**
 * Creates a [Date] from an ISO week date.
 * @param year the week-based year
 * @param week the week number of the week-based year
 * @param dayOfWeek the ISO day of week number, 1 (Monday) to 7 (Sunday)
 * @throws DateTimeException if the year, week, or day is invalid
 */
@OptIn(ExperimentalTime::class)
fun LocalDate.Companion.fromWeekDate(year: Int, week: Int, dayOfWeek: Int): LocalDate {
    //checkValidDayOfWeek(day)
    //checkValidYear(year)
    checkValidWeekOfWeekBasedYear(week, year)
    // TODO: The day number may exceed the max near the end of the year, but is it even worth checking?

    val jan4 = LocalDate(year, Month.JANUARY, 4)
    val dayOfYear = (week * 7 + dayOfWeek) - (jan4.dayOfWeek.isoDayNumber + 3)

    return if (dayOfYear < 1) {
        LocalDate.fromDayOfYear(year = year - 1, dayOfYear = dayOfYear + Year(year-1).days)
    } else {
        val lastDay = Year(year).days

        if (dayOfYear > lastDay) {
            LocalDate.fromDayOfYear(year = year + 1, dayOfYear = dayOfYear - lastDay)
        } else {
            LocalDate.fromDayOfYear(year, dayOfYear)
        }
    }
}

private fun checkValidWeekOfWeekBasedYear(week: Int): Int {
    if (week !in 1..53) {
        throw Exception("The week '$week' is outside the supported range of 1-53")
    }
    return week
}

@OptIn(ExperimentalTime::class)
private fun checkValidWeekOfWeekBasedYear(week: Int, year: Int): Int {
    checkValidWeekOfWeekBasedYear(week)

    if (week > Year(year).lastYearWeek()) {
        throw Exception("Week 53 doesn't exist in $year")
    }

    return week
}


@OptIn(ExperimentalTime::class)
fun LocalDate.Companion.fromDayOfYear(year: Int=1900, dayOfYear: Int) = Year(year).ofYearDay(dayOfYear)


fun LocalDate.atEndOfDay(hour: Int=23, minute: Int=59, second: Int = 59, nanosecond: Int = 0): LocalDateTime {
    return this.atTime(hour, minute, second, nanosecond)
}

fun LocalDate.atStartOfDay(hour: Int=0, minute: Int=0, second: Int = 0, nanosecond: Int = 0): LocalDateTime {
    return this.atTime(hour, minute, second, nanosecond)
}

fun LocalDate.Companion.parseOrNull(isoString: String): LocalDate? =
    runCatching { parse(isoString) }.getOrNull()

fun LocalDate.plusWeeks(weeks:Int):LocalDate = this.plus(weeks,DateTimeUnit.WEEK)
fun LocalDate.minusWeeks(weeks:Int):LocalDate = this.minus(weeks,DateTimeUnit.WEEK)
fun LocalDate.plusWeeks(weeks:Long):LocalDate = this.plus(weeks,DateTimeUnit.WEEK)
fun LocalDate.minusWeeks(weeks:Long):LocalDate = this.minus(weeks,DateTimeUnit.WEEK)



fun LocalDate.plusDays(days:Int):LocalDate = this.plus(days,DateTimeUnit.DAY)
fun LocalDate.minusDays(days:Int):LocalDate = this.minus(days,DateTimeUnit.DAY)
fun LocalDate.plusDays(days:Long):LocalDate = this.plus(days,DateTimeUnit.DAY)
fun LocalDate.minusDays(days:Long):LocalDate = this.minus(days,DateTimeUnit.DAY)


fun LocalDate.plusMonths(months:Int):LocalDate = this.plus(months,DateTimeUnit.MONTH)
fun LocalDate.minusMonths(months:Int):LocalDate = this.minus(months,DateTimeUnit.MONTH)
fun LocalDate.plusMonths(months:Long):LocalDate = this.plus(months,DateTimeUnit.MONTH)
fun LocalDate.minusMonths(months:Long):LocalDate = this.minus(months,DateTimeUnit.MONTH)


fun LocalDate.plusYears(years:Int):LocalDate = this.plus(years,DateTimeUnit.YEAR)
fun LocalDate.minusYears(years:Int):LocalDate = this.minus(years,DateTimeUnit.YEAR)
fun LocalDate.plusYears(years:Long):LocalDate = this.plus(years,DateTimeUnit.YEAR)
fun LocalDate.minusYears(years:Long):LocalDate = this.minus(years,DateTimeUnit.YEAR)


/**
 * Get the current date using provided [zone].
 */
 fun LocalDate.Companion.now(zone: TimeZone = currentTimeZone): LocalDate = LocalDateTime.now(zone).date

/**
 * Returns a new LocalDate with the day of month set to the specified [day]
 */
 fun LocalDate.withDayOfMonth(day: Int): LocalDate = LocalDate(year, month, day)

/**
 * Create a [LocalDateTime] with the specified [time]
 */
fun LocalDate.atTime(time: LocalTime): LocalDateTime = atTime(time.hour, time.minute, time.second)

fun LocalDate.atTime(time: LocalDateTime): LocalDateTime = atTime(time.time)

fun LocalDate.atTime(duration: Duration): LocalDateTime {
    return duration.toComponents { hours, minutes, seconds, nanoseconds ->
        atTime(hours.toInt() % 24, minutes, seconds, nanoseconds)
    }
//
//    val hours = duration.inWholeHours
//    val hour = maxOf(hours, 24).toInt()
//    var aux = duration - hours.hours
//    val minutes = aux.inWholeMinutes
//    val minute = minutes.toInt()
//    aux = aux - minutes.minutes
//    val second = aux.inWholeSeconds.toInt()
//    return atTime(hour, minute, second, (aux-second.seconds).inWholeNanoseconds.toInt())
}


private const val TOTAL_DAYS_IN_WEEK = 7

/**
 * Returns the next 7 dates starting from the current date.
 * @return A list of the next 7 dates.
 */
fun LocalDate.getNext7Dates() =
    List(TOTAL_DAYS_IN_WEEK) { day -> this.plus(day.toLong(), DateTimeUnit.DAY) }

fun LocalDate.copy(
    year: Int = this.year,
    month: Month = this.month,
    dayOfMonth: Int = this.dayOfMonth,
): LocalDate = try {
    LocalDate(year, month, dayOfMonth)
} catch (e: IllegalArgumentException) {
    LocalDate(year, month, 1)
}

@OptIn(ExperimentalTime::class)
fun LocalDate.withDayOfYear(dayOfYear:Int):LocalDate{
    if (this.dayOfYear == dayOfYear) {
        return this
    }
    return Year(year).ofYearDay(dayOfYear);
}

fun LocalDate.copyIfChanged(dayOfMonth: Int): LocalDate {
    return if (dayOfMonth == this.dayOfMonth) this else copy(dayOfMonth = dayOfMonth)
}

/**
 * Returns a copy of this date with the values of any individual components replaced by the new values specified.
 * @throws DateTimeException if the resulting date is invalid
 */
@OptIn(ExperimentalTime::class)
fun LocalDate.copy(
    year: Int = this.year,
    dayOfYear: Int,
) = Year(year).ofYearDay(dayOfYear);

/**
 * Computes the length of the current month of [LocalDate] in days
 */
 val LocalDate.lengthOfMonth: Int get() = month.length(year)

/**
 * Returns the previous 7 dates before the current date.
 * @return A list of the previous 7 dates.
 */
fun LocalDate.getPrevious7Dates() =
    List(TOTAL_DAYS_IN_WEEK) { day -> this.minus((day + 1), DateTimeUnit.DAY) }.reversed()

val ClosedRange<LocalDate>.days :Int get() = this.start.daysUntil(endInclusive)

val ClosedRange<LocalDate>.months :Int get() = this.start.monthsUntil(endInclusive)

val ClosedRange<LocalDate>.years :Int get() = this.start.yearsUntil(endInclusive)

val ClosedRange<LocalDate>.weeks :Long get() = this.start.until(endInclusive,DateTimeUnit.WEEK)

operator fun LocalDate.plus(dateTimePeriod: DateTimePeriod): LocalDateTime {
    return this.atStartOfDay() + dateTimePeriod
}

operator fun LocalDate.minus(dateTimePeriod: DateTimePeriod): LocalDateTime {
    return this.atStartOfDay() - dateTimePeriod
}

operator fun LocalDate.plus(months: MonthSpan): LocalDate = this + months.toDateTimePeriod()

operator fun LocalDate.plus(dateTimes: DateTimeSpan): LocalDateTime = this + dateTimes.toDateTimePeriod()


operator fun LocalDate.minus(months: MonthSpan): LocalDate = this - months.toDateTimePeriod()

operator fun LocalDate.minus(dateTimes: DateTimeSpan): LocalDateTime = this - dateTimes.toDateTimePeriod()

operator fun LocalDate.minus(other:LocalDate) = this - other.atStartOfDay()

operator fun LocalDate.minus(other:LocalDateTime) = this.atStartOfDay() - other

@OptIn(ExperimentalTime::class)
operator fun LocalDate.minus(other: Instant) = minus(other.toLocalDateTime(currentTimeZone))

operator fun LocalDate.minus(other: Duration) = this.atStartOfDay() - other

operator fun LocalDate.plus(other: Duration) = this.atStartOfDay().plus(other)

fun LocalDate.isMonday() = dayOfWeek == DayOfWeek.MONDAY
fun LocalDate.isTuesday() = dayOfWeek == DayOfWeek.TUESDAY
fun LocalDate.isWednesday() = dayOfWeek == DayOfWeek.WEDNESDAY
fun LocalDate.isThursday() = dayOfWeek == DayOfWeek.THURSDAY
fun LocalDate.isFriday() = dayOfWeek == DayOfWeek.FRIDAY
fun LocalDate.isSaturday() = dayOfWeek == DayOfWeek.SATURDAY
fun LocalDate.isSunday() = dayOfWeek == DayOfWeek.SUNDAY
fun LocalDate.isWeekend() = isSaturday() or isSunday()
fun LocalDate.isWeekday() = !isWeekend()


/**
 * Calculates the period between this date and another date as a `Period`.
 *
 *
 * This calculates the period between two dates in terms of years, months and days.
 * The start and end points are `this` and the specified date.
 * The result will be negative if the end is before the start.
 * The negative sign will be the same in each of year, month and day.
 *
 *
 * The calculation is performed using the ISO calendar system.
 * If necessary, the input date will be converted to ISO.
 *
 *
 * The start date is included, but the end date is not.
 * The period is calculated by removing complete months, then calculating
 * the remaining number of days, adjusting to ensure that both have the same sign.
 * The number of months is then normalized into years and months based on a 12 month year.
 * A month is considered to be complete if the end day-of-month is greater
 * than or equal to the start day-of-month.
 * For example, from `2010-01-15` to `2011-03-18` is "1 year, 2 months and 3 days".
 *
 *
 * There are two equivalent ways of using this method.
 * The first is to invoke this method.
 * The second is to use [Period.between]:
 * <pre>
 * // these two lines are equivalent
 * period = start.until(end);
 * period = Period.between(start, end);
</pre> *
 * The choice should be made based on which makes the code more readable.
 *
 * @param endDateExclusive  the end date, exclusive, which may be in any chronology, not null
 * @return the period between this date and the end date, not null
 */
fun LocalDate.until(end: LocalDate): Period {
    var totalMonths = end.getProlepticMonth() - this.getProlepticMonth() // safe
    var days: Int = end.day - day
    if (totalMonths > 0 && days < 0) {
        totalMonths--
        val calcDate = this.plusMonths(totalMonths)
        days = (end.toEpochDay() - calcDate.toEpochDay()).toInt() // safe
    } else if (totalMonths < 0 && days > 0) {
        totalMonths++
        days -= end.lengthOfMonth.toInt()
    }
    val years = totalMonths / 12 // safe
    val months = (totalMonths % 12).toInt() // safe
    return Period.of(Math.toIntExact(years), months, days)
}

//-----------------------------------------------------------------------
fun LocalDate.toEpochDay(): Long {
    val y = year.toLong()
    val m = month.number.toLong()
    var total: Long = 0
    total += 365 * y
    if (y >= 0) {
        total += (y + 3) / 4 - (y + 99) / 100 + (y + 399) / 400
    } else {
        total -= y / -4 - y / -100 + y / -400
    }
    total += ((367 * m - 362) / 12)
    total += (day - 1).toLong()
    if (m > 2) {
        total--
        if (!isLeapYear(y.toInt())) {
            total--
        }
    }
    return total - LocalDate.DAYS_0000_TO_1970
}

/**
 * The number of days in a 400 year cycle.
 */
internal val LocalDate.Companion.DAYS_PER_CYCLE: Int
    get() = 146097

/**
 * The number of days from year zero to year 1970.
 * There are five 400 year cycles from year zero to 2000.
 * There are 7 leap years from 1970 to 2000.
 */

val LocalDate.Companion.DAYS_0000_TO_1970: Long
    get() = (DAYS_PER_CYCLE * 5L) - (30L * 365L + 7L)


//-----------------------------------------------------------------------
/**
 * Obtains an instance of `LocalDate` from the epoch day count.
 *
 *
 * This returns a `LocalDate` with the specified epoch-day.
 * The [EPOCH_DAY][ChronoField.EPOCH_DAY] is a simple incrementing count
 * of days where day 0 is 1970-01-01. Negative numbers represent earlier days.
 *
 * @param epochDay  the Epoch Day to convert, based on the epoch 1970-01-01
 * @return the local date, not null
 * @throws DateTimeException if the epoch day exceeds the supported date range
 */
fun LocalDate.Companion.ofEpochDay(epochDay: Long): LocalDate {
    require(epochDay in -365243219162L..365241780471L)
    var zeroDay = epochDay + LocalDate.DAYS_0000_TO_1970
    // find the march-based year
    zeroDay -= 60 // adjust to 0000-03-01 so leap day is at end of four year cycle
    var adjust: Long = 0
    if (zeroDay < 0) {
        // adjust negative years to positive for calculation
        val adjustCycles = (zeroDay + 1) / LocalDate.DAYS_PER_CYCLE - 1
        adjust = adjustCycles * 400
        zeroDay += -adjustCycles * LocalDate.DAYS_PER_CYCLE
    }
    var yearEst = (400 * zeroDay + 591) / LocalDate.DAYS_PER_CYCLE
    var doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400)
    if (doyEst < 0) {
        // fix estimate
        yearEst--
        doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400)
    }
    yearEst += adjust // reset any negative year
    val marchDoy0 = doyEst.toInt()

    // convert march-based values back to january-based
    val marchMonth0 = (marchDoy0 * 5 + 2) / 153
    var month = marchMonth0 + 3
    if (month > 12) {
        month -= 12
    }
    val dom = marchDoy0 - (marchMonth0 * 306 + 5) / 10 + 1
    if (marchDoy0 >= 306) {
        yearEst++
    }

    return LocalDate(yearEst.toInt(), month, dom)
}

fun T.firstWeekInYearStart(year: Int): LocalDate {
    val jan1st = LocalDate(year, 1, 1)
    val previousMonday = jan1st.minus(jan1st.dayOfWeek.ordinal, DateTimeUnit.DAY)
    return if (jan1st.dayOfWeek <= DayOfWeek.THURSDAY) {
        previousMonday
    } else {
        previousMonday.plus(
            1,
            DateTimeUnit.WEEK,
        )
    }
}


fun LocalDate.isoWeekNumber(): Long {
    if (T.firstWeekInYearStart(year + 1) < this) return 1
    val currentYearStart = T.firstWeekInYearStart(year)
    val start = if (this < currentYearStart) T.firstWeekInYearStart(year - 1) else currentYearStart
    val currentCalendarWeek = start.until(this, DateTimeUnit.WEEK) + 1
    return currentCalendarWeek
}

fun LocalDate.at(firstDayOfWeek: DayOfWeek): LocalDate {
    var date = this
    while (date.dayOfWeek != firstDayOfWeek) {
        date = date.plus(1, DateTimeUnit.DAY)
    }
    return date
}

fun LocalDate.firstDayOfWeekInMonth(startDayOfWeek: DayOfWeek): LocalDate {
    val firstDayOfMonth = LocalDate(year, month, 1)
    var date = firstDayOfMonth
    while (date.dayOfWeek != startDayOfWeek) {
        date = date.plus(1, DateTimeUnit.DAY)
    }
    return date
}

fun LocalDate.firstDayOfWeekInThisWeek(startDayOfWeek: DayOfWeek): LocalDate {
    var date = this
    while (date.dayOfWeek != startDayOfWeek) {
        date = date.minus(1, DateTimeUnit.DAY)
    }
    return date
}

fun LocalDate.endOfWeekInThisWeek(startDayOfWeek: DayOfWeek): LocalDate {
    var date = this
    if (date.dayOfWeek == startDayOfWeek) {
        return date.plus(DatePeriod(days = 6))
    }
    while (date.dayOfWeek != startDayOfWeek) {
        date = date.plus(DatePeriod(days = 1))
    }
    date.minus(DatePeriod(days = 1))
    return date
}

fun LocalDate.firstDayOfThisQuarter(): LocalDate {
    val firstMonthInQuarter =
        when (quarter) {
            1 -> 1
            2 -> 4
            3 -> 7
            4 -> 10
            else -> throw IllegalArgumentException("Invalid quarter: $quarter. Valid values are 1-4.")
        }
    return LocalDate(year, firstMonthInQuarter, 1)
}

//val LocalDate.quarter get() = (monthNumber - 1) / 3 + 1

@OptIn(ExperimentalTime::class)
fun LocalDate.toEpochMilliseconds(): Long = this.atStartOfDayIn(TimeZone.currentSystemDefault()).toEpochMilliseconds()
